Нам предстоит изучить данные стартапа, которы продаёт продукты питания. Наша задача:
Изучить, оттолкнет ли пользователей внесение изменений в шрифты приложения.
Для этого нам предстоит изучить логи событий приложения, которые нам предоставили. Всех пользователей разбили на 3 группы: 2 контрольные со старыми шрифтами и одну экспериментальную — с новыми. Это было сделано для уверенности в точности проведенного тестирования.
Для достижения нашей задачи следует провести следующие действия:
Приступим к анализу данных.
Загрузим необходимые для работы библиотеки
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats as st
import numpy as np
import math as mth
import datetime as dt
from pandas.plotting import register_matplotlib_converters
import warnings
import plotly.express as px
from plotly import graph_objects as go
from statsmodels.sandbox.stats.multicomp import multipletests
data = pd.read_csv('D:\\Irina\\datasets\\logs_exp.csv', sep='\t')
#выводим все колонки
pd.set_option('display.max_columns', None)
#установим максимальное количество символов в колонке
pd.options.display.max_colwidth = 100
display(data.head(10))
data.info()
| EventName | DeviceIDHash | EventTimestamp | ExpId | |
|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 |
| 1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 |
| 3 | CartScreenAppear | 3518123091307005509 | 1564054127 | 248 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 1564055322 | 248 |
| 5 | CartScreenAppear | 6217807653094995999 | 1564055323 | 248 |
| 6 | OffersScreenAppear | 8351860793733343758 | 1564066242 | 246 |
| 7 | MainScreenAppear | 5682100281902512875 | 1564085677 | 246 |
| 8 | MainScreenAppear | 1850981295691852772 | 1564086702 | 247 |
| 9 | MainScreenAppear | 5407636962369102641 | 1564112112 | 246 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 244126 entries, 0 to 244125 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 EventName 244126 non-null object 1 DeviceIDHash 244126 non-null int64 2 EventTimestamp 244126 non-null int64 3 ExpId 244126 non-null int64 dtypes: int64(3), object(1) memory usage: 7.5+ MB
Итак, у нас есть лог приложения. Каждая запись в логе — это действие пользователя, или событие. Всего у нас есть 4 колонки:
EventName — название события; DeviceIDHash — уникальный идентификатор пользователя; EventTimestamp — время события; ExpId — номер эксперимента: 246 и 247 — контрольные группы, а 248 — экспериментальная.В датафрейме есть 244126 записей, и кажется, нет пропусков. Первое, что бросается в глаза - даты представлены в формате timestamp с типом int. Для работы нам этот вариант не подходит, поэтому позднее мы поправим этот момент. Приступим к предобработке.
Прежде чем мы приступим к обработке данных, изменим названия колонок в соответствии со стилем Snake case. Сделаем это для удобства работы.
data.columns = ['event_name','device_id','event_timestamp','exp_id']
data.head()
| event_name | device_id | event_timestamp | exp_id | |
|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 |
| 1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 |
| 3 | CartScreenAppear | 3518123091307005509 | 1564054127 | 248 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 1564055322 | 248 |
Предварительно мы сделали предположение, что в данных нет пропусков, проверим данное утверждение.
data.isna().sum()
event_name 0 device_id 0 event_timestamp 0 exp_id 0 dtype: int64
Что же, у нас нет пропусков, это несомненно радует, потому что заменить пропуски не представлялось бы возможным из-за особенностей данных.
Проверим наличие дубликатов в датафрейме, как явных, так и неявных.
data.duplicated().sum()
413
Итак, у нас есть 413 полных дубликатов, это составляет 0.17% от всего количества данных, так что смело избавимся от этих дубликатов, сохранив первое "вхождение" данных. И проверим, есть ли неявные дубликаты.
#Удалим дубликаты с обновлением индексов
data.drop_duplicates(keep='first').reset_index()
#Проверим наличие неявных дубликатов, оценив наличие ошибок в наименованиях события
data['event_name'].unique()
array(['MainScreenAppear', 'PaymentScreenSuccessful', 'CartScreenAppear',
'OffersScreenAppear', 'Tutorial'], dtype=object)
Дубликатов, образовавшихся из-за орфографических ошибок или расхождений регистра нет. Значит двигаемся дальше.
Итак, пришло время решить проблему с датами. Для этого создадим два новых столбца, в которые войдут дата и время и отдельно только дата. Для этого нужно перевести данные из формата timestamp в привычный нам формат даты.
data['event_dt'] = pd.to_datetime(data['event_timestamp'], unit='s')
data['date'] = data['event_dt'].dt.floor('D')
data.head()
| event_name | device_id | event_timestamp | exp_id | event_dt | date | |
|---|---|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 | 2019-07-25 04:43:36 | 2019-07-25 |
| 1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 | 2019-07-25 11:11:42 | 2019-07-25 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 | 2019-07-25 11:28:47 | 2019-07-25 |
| 3 | CartScreenAppear | 3518123091307005509 | 1564054127 | 248 | 2019-07-25 11:28:47 | 2019-07-25 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 1564055322 | 248 | 2019-07-25 11:48:42 | 2019-07-25 |
Теперь все данные приведены в порядок и мы можем приступить к анализу данных.
Теперь проанализируем, сколько у нас есть событий, пользователей. Какой период времени нам предоставлен. Оценим, есть ли аномальные данные и избавимся от них.
print('Общее количество событий:', len(data['event_name']))
display('Количество каждого вида событий:', data['event_name'].value_counts())
data['event_name'].value_counts().plot(kind='pie',
ylabel='',
autopct='%1.1f%%',
figsize=(10,10),
fontsize=12,
colormap = 'Accent');
plt.title('Соотношение видов событий', fontsize=16);
Общее количество событий: 244126
'Количество каждого вида событий:'
MainScreenAppear 119205 OffersScreenAppear 46825 CartScreenAppear 42731 PaymentScreenSuccessful 34313 Tutorial 1052 Name: event_name, dtype: int64
Итак, за исследуемое время было совершено 244126 событий. Всего у нас представлено 5 уникальных событий:
MainScreenAppear - открытие главной страницы приложения - самое популярное событие, что не удивительно, ведь без этого невозможно что-либо сделать в приложении. Его доля от остальных событий составляет 48.8%;OffersScreenAppear - экран продукта - второй по популярности - пользователи изучают подробности продуктов и после могут добавить их в корзину;CartScreenAppear - Окно корзины - третье по популярности событие;PaymentScreenSuccesful - экран успешной оплаты покупки;Tutorial - инструкция по использованию приложения - самое непопулярное событие, меньше всего пользователей изучают, как пользоваться приложением.print('Общее количество уникальных пользователей:', data['device_id'].nunique())
user_data = data.groupby('exp_id').agg({'device_id':'nunique'}).reset_index()
display(user_data)
user_data.plot.bar(x='exp_id',
y='device_id',
legend=False,
rot=0,
color=['lightgreen','navajowhite','saddlebrown'],
xlabel='Номер эксперимента',
ylabel='Количество пользователей')
plt.title('Количество уникальных пользователей в эксперименте');
Общее количество уникальных пользователей: 7551
| exp_id | device_id | |
|---|---|---|
| 0 | 246 | 2489 |
| 1 | 247 | 2520 |
| 2 | 248 | 2542 |
В исследуемый момент приложением пользуется 7551 пользователь. Всего пользователи разделены на 3 группы, в каждой из которой есть около 2.5 тысяч пользователей. Оценим, сколько событий совершают пользователи.
events_per_user = data.pivot_table(index='device_id',values='event_name',aggfunc='count')
print('В среднем на пользователя приходится {} события'.format(round(events_per_user['event_name'].mean())))
events_per_user.hist(bins=50, color='Orange')
plt.title('Число событий на пользователя')
plt.xlabel('Число событий')
plt.ylabel('Число пользователей');
display(events_per_user.describe())
В среднем на пользователя приходится 32 события
| event_name | |
|---|---|
| count | 7551.000000 |
| mean | 32.330287 |
| std | 65.312344 |
| min | 1.000000 |
| 25% | 9.000000 |
| 50% | 20.000000 |
| 75% | 37.500000 |
| max | 2308.000000 |
Большая часть пользователей совершает не более 37 событий. В среднем пользователи совершают около 32 событий. Однако, настораживает, что какие-то пользователи совершают около 2300 событий. Конечно всё возможно, может кто-то завел корпоративный аккаунт и заказывает обеды в компанию, или это аккаунт какого-нибудь кафе, которое заказывает продукты для готовки. Согласно графику таких пользователей крайне мало. С точностью сказать, являются ли эти данные аномальными, нельзя. Посмотрим чуть поближе, "срезав" слишком большое количество событий.
events_per_user.query('event_name < 100')['event_name'].hist(bins=25, color='Green')
plt.title('Число событий на пользователя')
plt.xlabel('Число событий')
plt.ylabel('Число пользователей')
plt.axvline(x=32,color='black',linestyle='--');
При более детальном взгляде, можно отметить, что основная масса пользователей совершает около 20 событий, и крайне мало пользователей доходят до 100 событий.
Теперь изучим, данные за какой период представлены в логе.
print('Первая дата события:', data['date'].min(),
'\nПоследняя дата события:', data['date'].max())
Первая дата события: 2019-07-25 00:00:00 Последняя дата события: 2019-08-07 00:00:00
Итак, в нашем распоряжении есть две недели, но стоит проверить, есть ли данные на протяжении всех 14 дней.
data['date'].hist(bins=50, figsize=(15,5))
plt.xlabel('Дата')
plt.ylabel('Количество событий')
plt.title('Распределение событий по датам');
Что же, полные данные присутствуют только в последние 7 дней логов - с 1 по 7 августа 2019 года включительно. Теперь стоит изучить, есть ли данные всех групп в этот промежуток.
group_data = data.pivot_table(values='event_name', index='date',columns='exp_id', aggfunc='count')
group_data.columns = ['246','247','248']
group_data.plot.barh(figsize=(6,10), xlabel='Дата', colormap='Pastel2')
plt.xlabel('Количество событий')
plt.title('Количество событий в зависимости от времени в разрезе групп');
Отлично, во все интересующие нас дни анализа присутствовали пользователи всех трёх групп. Теперь пришло время избавиться от аномалий.
Для того, чтобы избавиться от аномалий, мы сделаем срез нашего датафрейма и удалим данные за первые семь дней, так как они неполные и не интересуют нас.
clear_data = data.query('date > "2019-07-31"').reset_index(drop=True)
clear_data.head()
| event_name | device_id | event_timestamp | exp_id | event_dt | date | |
|---|---|---|---|---|---|---|
| 0 | Tutorial | 3737462046622621720 | 1564618048 | 246 | 2019-08-01 00:07:28 | 2019-08-01 |
| 1 | MainScreenAppear | 3737462046622621720 | 1564618080 | 246 | 2019-08-01 00:08:00 | 2019-08-01 |
| 2 | MainScreenAppear | 3737462046622621720 | 1564618135 | 246 | 2019-08-01 00:08:55 | 2019-08-01 |
| 3 | OffersScreenAppear | 3737462046622621720 | 1564618138 | 246 | 2019-08-01 00:08:58 | 2019-08-01 |
| 4 | MainScreenAppear | 1433840883824088890 | 1564618139 | 247 | 2019-08-01 00:08:59 | 2019-08-01 |
print('Количество строк до фильтрации:', len(data),
'\nКоличество строк после фильтрации:', len(clear_data))
print('Удалено {} строк, что составляет {:.2%}'.format((len(data)-len(clear_data)),
(len(data) - len(clear_data)) / len(data)))
Количество строк до фильтрации: 244126 Количество строк после фильтрации: 241298 Удалено 2828 строк, что составляет 1.16%
Количество событий сократилось чуть больше, чем на 1%. Это здорово, наше вмешательство не будет вносить серьезных изменений.
print('Количество уникальных пользователей до фильтрации:', data['device_id'].nunique(),
'\nКоличество уникальных пользователей после фильтрации:', clear_data['device_id'].nunique())
print('Удалено {} пользователей, что составляет {:.2%}'.format((data['device_id'].nunique()-clear_data['device_id'].nunique()),
(data['device_id'].nunique() - clear_data['device_id'].nunique()) / data['device_id'].nunique()))
Количество уникальных пользователей до фильтрации: 7551 Количество уникальных пользователей после фильтрации: 7534 Удалено 17 пользователей, что составляет 0.23%
Количество пользователей сократилось менее чем на 0.5%, скорее всего это пользователи, которые попали случайно из других исследований, или из-за того, что взят неправильный временной промежуток.
Когда мы убедились, что все данные в порядке, приступим к анализу воронки событий.
Вновь взглянем на наши события.
print('Общее количество событий:', len(clear_data['event_name']))
display('Количество каждого вида событий:', clear_data['event_name'].value_counts())
clear_data['event_name'].value_counts().plot(kind='pie',
ylabel='',
autopct='%1.1f%%',
figsize=(9,9),
fontsize=12,
colormap = 'Accent');
plt.title('Соотношение видов событий', fontsize=16);
Общее количество событий: 241298
'Количество каждого вида событий:'
MainScreenAppear 117431 OffersScreenAppear 46350 CartScreenAppear 42365 PaymentScreenSuccessful 34113 Tutorial 1039 Name: event_name, dtype: int64
Итак, теперь перед нами 241298 событий, часть нам пришлось удалить на этапе очистки от аномалий. У нас по-прежнему есть 5 уникальных событий.
MainScreenAppear - Самое часто совершаемое событие, без него практически невозможно совершить следующие события - 48.7%;OffersScreenAppear - экран продукта для заказа на втором месте - 19.2%;CartScreenAppear - экран корзины заказов - 17.6%;PaymentScreenSuccesful - экран успешной оплаты покупки - 14.1%Tutorial - инструкция по использованию приложения - 0.4%.Доли событий почти не изменились.
user_logs = (clear_data.pivot_table(index='event_name',values='device_id',aggfunc='nunique')
.sort_values(by='device_id',ascending=False))
user_logs['at_least_1_time_%'] = round((user_logs['device_id']/clear_data['device_id'].nunique())*100,2)
display(user_logs)
| device_id | at_least_1_time_% | |
|---|---|---|
| event_name | ||
| MainScreenAppear | 7419 | 98.47 |
| OffersScreenAppear | 4593 | 60.96 |
| CartScreenAppear | 3734 | 49.56 |
| PaymentScreenSuccessful | 3539 | 46.97 |
| Tutorial | 840 | 11.15 |
Итак, всего у нас есть 7534 пользователя, из них только 98.36% (7419 пользователей) заходят на главный экран. Около 60% пользователей переходят на экран продукта для заказа. И только половина пользователей заходит в корзину или совершает оплату покупки. Меньше всего пользователей изучают, как работать с приложением. В таблице в графе at_least_1_time_% указано какой процент пользователей совершает событие хотябы раз.
Предположим, что пользователи совершают следующую последовательность действий:
Таким образом мы получим следующую последовательность событий: MainScreenAppear -> OffersScreenAppear -> CartScreenAppear -> PaymentScreenSuccessful. Событие Tutorial не попадает в данную последовательность действий, так как оно может быть совершено пользователем в любой момент времени и от него не зависит совершение ключевого события - успешной оплаты заказа. Таким образом, изучим получившуюся воронку событий, без учета события Tutorial.
event_funnel = (clear_data.query('event_name !="Tutorial"')
.pivot_table(index='event_name',values='device_id',aggfunc='nunique')
.sort_values(by='device_id',ascending=False))
event_funnel['conversion_per_step'] = 0
for i in range(0, len(event_funnel['device_id'])):
if i == 0:
event_funnel['conversion_per_step'].iloc[i] = 100
else:
event_funnel['conversion_per_step'].iloc[i] = round(
(event_funnel['device_id'].iloc[i] / event_funnel['device_id'].iloc[i-1])*100, 2)
event_funnel
| device_id | conversion_per_step | |
|---|---|---|
| event_name | ||
| MainScreenAppear | 7419 | 100.00 |
| OffersScreenAppear | 4593 | 61.91 |
| CartScreenAppear | 3734 | 81.30 |
| PaymentScreenSuccessful | 3539 | 94.78 |
Итак, мы выяснили, что на экран заказа продукта переходит только около 62% пользователей. Уже на этом этапе теряются пользователи. Из них 81% переходит в корзину. И почти 95% пользователей, которые перешли в корзину, совершают покупки.
print('От первого события до оплаты доходят {:.2%} пользователей'.format(
event_funnel['device_id'].iloc[3]/event_funnel['device_id'].iloc[0]))
От первого события до оплаты доходят 47.70% пользователей
Всего, от запуска приложения (просмотра главного экрана) до покупки в приложении доходят только 47,7% пользователей. Чуть меньше половины. Да, не идеальный показатель, но все равно очень хороший. С этим можно работать. Конечно мы не знаем других метрик приложения и не можем оценить его эффективность. Но в данный момент нас интересует, не отпугнёт ли наше нововведение пользователей и не уменьшит ли показатель конверсии.
Визуализируем воронку событий.
fig = go.Figure(go.Funnel(
y = ['MainScreenAppear','OffersScreenAppear','CartScreenAppear','PaymentScreenSuccessful'],
x = event_funnel['device_id'],
textposition = "inside",
textinfo = "value+percent previous"))
fig.update_layout(
title='Воронка событий',
yaxis_title='События',
autosize=False,
width=1000,
height=500,
font=dict(
family="arial",
size=18,
color="darkslateblue"
))
fig.show()
Теперь визуализируем её по группам
group_event_funnel = (clear_data.query('event_name != "Tutorial"')
.pivot_table(index='event_name', values='device_id',columns='exp_id', aggfunc='nunique')
.reset_index()
.sort_values(by=[246], ascending=False))
fig = go.Figure()
fig.add_trace(go.Funnel(
name = 'Группа 246',
y = ['MainScreenAppear','OffersScreenAppear','CartScreenAppear','PaymentScreenSuccessful'],
x = group_event_funnel[246],
textinfo = "value+percent previous"))
fig.add_trace(go.Funnel(
name = 'Группа 247',
orientation = "h",
y = ['MainScreenAppear','OffersScreenAppear','CartScreenAppear','PaymentScreenSuccessful'],
x = group_event_funnel[247],
textposition = "inside",
textinfo = "value+percent previous"))
fig.add_trace(go.Funnel(
name = 'Группа 248',
orientation = "h",
y = ['MainScreenAppear','OffersScreenAppear','CartScreenAppear','PaymentScreenSuccessful'],
x = group_event_funnel[248],
textposition = "inside",
textinfo = "value+percent previous"))
fig.update_layout(
title='Воронка событий по группам',
yaxis_title='События',
autosize=False,
width = 1000,
height=500,
font=dict(
family="arial",
size=18,
color="darkslateblue"
))
fig.show()
Для начала проверим, сколько у нас есть пользователей в каждой исследуемой группе после очистки данных.
user_clear_data = clear_data.groupby('exp_id').agg({'device_id':'nunique'})
display(user_clear_data)
user_clear_data.plot.bar(
y='device_id',
legend=False,
rot=0,
color=['lightgreen','navajowhite','saddlebrown'],
xlabel='Номер эксперимента',
ylabel='Количество пользователей')
plt.title('Количество уникальных пользователей в эксперименте');
| device_id | |
|---|---|
| exp_id | |
| 246 | 2484 |
| 247 | 2513 |
| 248 | 2537 |
Проверим, встречаются ли одни и те же уникальные пользователи в разных группах, чтобы избежать проблемы подглядывания.
#Получим таблицу со списками уникальных пользователей по группам
user_list = clear_data.groupby('exp_id')['device_id'].unique()
#Получим списки, в которые будут добавляться значениия, встречающихся в обоих списках
list_of_repeats1 = list(set(user_list[246]) & set(user_list[247]))
list_of_repeats2 = list(set(user_list[247]) & set(user_list[248]))
list_of_repeats3 = list(set(user_list[246]) & set(user_list[248]))
#Объедиим их в один список
common_repeats = [*list_of_repeats1, *list_of_repeats2, *list_of_repeats3]
#Зададим условие, проверяющее длину списка
if len(common_repeats) == 0:
print('Уникальные пользователи в группах не пересекаются')
else:
print('Идентификаторы пользователей, которых стоит удалить:', common_repeats)
Уникальные пользователи в группах не пересекаются
Разница между "сырыми" и очищенными данными минимальна, ведь мы избавились только от 17 пользователей и в каждой группе около 2500 пользователей. Каждый пользователь принадлежит только одной группе. Теперь оценим сколько пользователей в каждой группе совершали определенные события
exp_groups = clear_data.pivot_table(index='event_name', values='device_id',columns='exp_id', aggfunc='nunique')
display(exp_groups)
exp_groups.plot(kind='barh', xlabel='Событие')
plt.title('Количество пользователей, совершивших событие в зависимости от группы')
plt.xlabel('Количество пользователей');
| exp_id | 246 | 247 | 248 |
|---|---|---|---|
| event_name | |||
| CartScreenAppear | 1266 | 1238 | 1230 |
| MainScreenAppear | 2450 | 2476 | 2493 |
| OffersScreenAppear | 1542 | 1520 | 1531 |
| PaymentScreenSuccessful | 1200 | 1158 | 1181 |
| Tutorial | 278 | 283 | 279 |
В каждой группе примерно одинаковое количество пользователей совершает схожие события. Самым популярным событием в каждой группе является переход на главный экран приложения, его совершают почти все пользователи каждой группы. Вычислим, какая доля пользователей в каждой группе совершала это действие. Для этого немного видоизменим нашу таблицу.
#"Развернем" полученную ранее таблицу
event_by_experiment = exp_groups.T
#Добавим недостающие данные - общее число пользователей
event_by_experiment['total_users'] = user_clear_data['device_id']
event_by_experiment
| event_name | CartScreenAppear | MainScreenAppear | OffersScreenAppear | PaymentScreenSuccessful | Tutorial | total_users |
|---|---|---|---|---|---|---|
| exp_id | ||||||
| 246 | 1266 | 2450 | 1542 | 1200 | 278 | 2484 |
| 247 | 1238 | 2476 | 1520 | 1158 | 283 | 2513 |
| 248 | 1230 | 2493 | 1531 | 1181 | 279 | 2537 |
Теперь, когда с таблицей работать стало удобнее, посчитаем доли пользователей.
print('Доля пользователей, открывших главный экран:',
'\nВ группе 246: {:.2%}'.format(
event_by_experiment['MainScreenAppear'][246]/event_by_experiment['total_users'][246]),
'\nВ группе 247: {:.2%}'.format(
event_by_experiment['MainScreenAppear'][247]/event_by_experiment['total_users'][247]),
'\nВ группе 248: {:.2%}'.format(
event_by_experiment['MainScreenAppear'][248]/event_by_experiment['total_users'][248]))
Доля пользователей, открывших главный экран: В группе 246: 98.63% В группе 247: 98.53% В группе 248: 98.27%
Итак, во всех группах самое популярное событие совершают около 98% пользователей.
Настало время сравнить, есть ли статистически значимые различия между этими группами в совершении определенных событий. Но для начала, чтобы провести тест, добавим в нашу таблицу строку с объединенными данными по двум контрольным группам - 246 и 247. Назовём полученную группу 493 (как сумма 246 и 247), чтобы было удобно с ней работать.
event_by_experiment.loc[493] = event_by_experiment.loc[246] + event_by_experiment.loc[247]
event_by_experiment
| event_name | CartScreenAppear | MainScreenAppear | OffersScreenAppear | PaymentScreenSuccessful | Tutorial | total_users |
|---|---|---|---|---|---|---|
| exp_id | ||||||
| 246 | 1266 | 2450 | 1542 | 1200 | 278 | 2484 |
| 247 | 1238 | 2476 | 1520 | 1158 | 283 | 2513 |
| 248 | 1230 | 2493 | 1531 | 1181 | 279 | 2537 |
| 493 | 2504 | 4926 | 3062 | 2358 | 561 | 4997 |
Проводить оценку статистической значимости мы будем с использованием z-теста для сравнения долей выборок, так как наши выборки не совсем идентичны, а поведение людей трудно поддаётся нормальному распределению.
Так как нам предстоит проверить несколько выборок по нескольким параметрам, постараемся максимально автоматизировать процесс. Для начала зададим функцию, которая будет автоматизировать проведение z-теста.
#Функция, которая проводит z-тестирование
def z_test(hits, trials, alpha):
# доля успехов в исследуемых группах:
p1 = hits[0]/trials[0]
p2 = hits[1]/trials[1]
# доля успехов в комбинированном датасете:
p_combined = (hits[0] + hits[1]) / (trials[0] + trials[1])
# разница долей в датасетах
difference = p1 - p2
# считаем статистику в ст.отклонениях стандартного нормального распределения
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/trials[0] + 1/trials[1]))
# задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
distr = st.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
print('p-значение: ', p_value)
if p_value < alpha:
print('Отвергаем нулевую гипотезу: между долями есть значимая разница')
else:
print('Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными')
#вернем значение p_value для поправок на множественное тестирование
return p_value
Теперь зададим функцию, которая будет передавать функции, которая выполняет тестирование, параметры для проверки. В тело функции зададим списки с количествами пользователей тестируемых групп для каждого события, которые будут принимать значение одной группы и второй сравниваемой группы.
#Создадим функцию, которая будет проводить z-тест для всех параметров по группам
#На вход будет принимать номера экспериментов и значение статистической значимости
def lazy_check (n_1, n_2, alpha):
#Зададим переменные, которые будут принимать значения p_value для поправок на множественное
#проведение гипотез
pv1 = 0
pv2 = 0
pv3 = 0
pv4 = 0
#Зададим список значений trials для обоих экспериментов (общее количество пользователей каждого эксперимента)
n_trials = ([event_by_experiment['total_users'][n_1],
event_by_experiment['total_users'][n_2]])
#Теперь получим списки значений hits по каждой исследуемой выборке
main_screen = ([event_by_experiment['MainScreenAppear'][n_1],
event_by_experiment['MainScreenAppear'][n_2]])
offer_screen = ([event_by_experiment['OffersScreenAppear'][n_1],
event_by_experiment['OffersScreenAppear'][n_2]])
cart_screen = ([event_by_experiment['CartScreenAppear'][n_1],
event_by_experiment['CartScreenAppear'][n_2]])
payment = ([event_by_experiment['PaymentScreenSuccessful'][n_1],
event_by_experiment['PaymentScreenSuccessful'][n_2]])
#Автоматизируем вывод результатов тестирования
print('Сравнение долей по пользователям, открывшим главную страницу:')
pv1 = z_test(main_screen, n_trials, alpha)
print('')
print('Сравнение долей по пользователям, открывшим страницу товара:')
pv2 = z_test(offer_screen, n_trials, alpha)
print('')
print('Сравнение долей по пользователям, перешедшим в корзину:')
pv3 = z_test(cart_screen, n_trials, alpha)
print('')
print('Сравнение долей по пользователям, совершившим покупку:')
pv4 = z_test(payment, n_trials, alpha)
#Вернем список p_value
return [pv1, pv2, pv3, pv4]
Теперь нам необходимо провести поочередную попарную проверку наших выборок.
493) c экспериментальной выборкой.Для всех тестирований ниже примем следующие гипотезы:
Сравним группы 246 и 247 со старыми шрифтами между собой. Чтобы исключить возникновение ошибок.
# Сделаем проверку контрольных групп А/A
#Получим лист p_value
lp1_05 = 0
lp1_05 = lazy_check(246, 247, 0.05)
Сравнение долей по пользователям, открывшим главную страницу: p-значение: 0.7570597232046099 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Сравнение долей по пользователям, открывшим страницу товара: p-значение: 0.2480954578522181 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Сравнение долей по пользователям, перешедшим в корзину: p-значение: 0.22883372237997213 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Сравнение долей по пользователям, совершившим покупку: p-значение: 0.11456679313141849 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
Для начала мы задали статистическую значимость в 5%. То есть с вероятностью в 5% мы можем ошибочно отвергнуть гипотезу. Тестирование показало, что во всех случаях исследуемые доли в обеих выборках не имеют статистически значимых различий. Значит можно считать выборки равными и переходить к А/В - тестированию.
Но перед этим проверим, есть ли вероятность отвергнуть нулевую гипотезу при статистической значимости в 10%.
lp1_10 = 0
lp1_10 = lazy_check(246, 247, 0.1)
Сравнение долей по пользователям, открывшим главную страницу: p-значение: 0.7570597232046099 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Сравнение долей по пользователям, открывшим страницу товара: p-значение: 0.2480954578522181 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Сравнение долей по пользователям, перешедшим в корзину: p-значение: 0.22883372237997213 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Сравнение долей по пользователям, совершившим покупку: p-значение: 0.11456679313141849 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
В данной ситуации мы также не отвергаем нулевую гипотезу, значит мы смело можем продолжать наше тестирование.
Теперь перейдём к сравнению контрольных выборок с экспериментальной. Начнем с выборки 246 (примем ее а А1). Проведем тестирование сразу с двумя вариантами статистической значимости.
#Сделаем проверку контрольной группы 246 с экспериментальной 248
lp2_05 = 0
lp2_05 = lazy_check(246, 248, 0.05)
Сравнение долей по пользователям, открывшим главную страницу: p-значение: 0.2949721933554552 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Сравнение долей по пользователям, открывшим страницу товара: p-значение: 0.20836205402738917 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Сравнение долей по пользователям, перешедшим в корзину: p-значение: 0.07842923237520116 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Сравнение долей по пользователям, совершившим покупку: p-значение: 0.2122553275697796 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
Итак, при статичтической значимости в 5% разницы между проверяемыми гипотезами нет.
lp2_10 = 0
lp2_10 = lazy_check(246, 248, 0.1)
Сравнение долей по пользователям, открывшим главную страницу: p-значение: 0.2949721933554552 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Сравнение долей по пользователям, открывшим страницу товара: p-значение: 0.20836205402738917 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Сравнение долей по пользователям, перешедшим в корзину: p-значение: 0.07842923237520116 Отвергаем нулевую гипотезу: между долями есть значимая разница Сравнение долей по пользователям, совершившим покупку: p-значение: 0.2122553275697796 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
А вот при статистической значимости в 10%, наблюдаются различия в выборках по показателю перехода пользователей в корзину. Посмотрим, как отличаются эти группы.
print('В контрольной группе {:.2%} пользователей переходят в корзину'.format(
event_by_experiment['CartScreenAppear'].loc[246]/event_by_experiment['total_users'].loc[246]),
'\nВ экспериментальной группе {:.2%} пользователей переходят в корзину'. format(
event_by_experiment['CartScreenAppear'].loc[248]/event_by_experiment['total_users'].loc[248]))
В контрольной группе 50.97% пользователей переходят в корзину В экспериментальной группе 48.48% пользователей переходят в корзину
В экспериментальной группе пользователи менее активно переходят в корзину и это имеет статистическую значимость. Однако, это не влияет на совершение покупок в контрольной группе. При статистической значимости в 5% выборки не отличаются.
Теперь проведем сравнение второй контрольной группой с экспериментальной. Также проведем с двумя разными значениями статистической значимости.
#Сделаем проверку контрольной группы 247 с экспериментальной 248
lp3_05 = 0
lp3_05 = lazy_check(247, 248, 0.05)
Сравнение долей по пользователям, открывшим главную страницу: p-значение: 0.4587053616621515 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Сравнение долей по пользователям, открывшим страницу товара: p-значение: 0.9197817830592261 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Сравнение долей по пользователям, перешедшим в корзину: p-значение: 0.5786197879539783 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Сравнение долей по пользователям, совершившим покупку: p-значение: 0.7373415053803964 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
В данном случае разницы между группами не отмечается.
lp3_10 = 0
lp3_10 = lazy_check(247, 248, 0.1)
Сравнение долей по пользователям, открывшим главную страницу: p-значение: 0.4587053616621515 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Сравнение долей по пользователям, открывшим страницу товара: p-значение: 0.9197817830592261 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Сравнение долей по пользователям, перешедшим в корзину: p-значение: 0.5786197879539783 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Сравнение долей по пользователям, совершившим покупку: p-значение: 0.7373415053803964 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
И в случае 10% статистической значимости разницы между группами нет. А вот в группе 246 почему-то есть различия, хотя они не влияют на ключевое событие. Теперь перейдем к сравнению экспериментальной группы с объединенными контрольными группами.
И наконец, мы переходим к тестированию объединенных контрольных групп с экспериментальной.
#Сравним объединенную контрольную выборку 493 с экспериментальной 248
lp4_05 = 0
lp4_05 = lazy_check(493, 248, 0.05)
Сравнение долей по пользователям, открывшим главную страницу: p-значение: 0.29424526837179577 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Сравнение долей по пользователям, открывшим страницу товара: p-значение: 0.43425549655188256 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Сравнение долей по пользователям, перешедшим в корзину: p-значение: 0.18175875284404386 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Сравнение долей по пользователям, совершившим покупку: p-значение: 0.6004294282308704 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
При статистической значимости в 5% разницы между исследуемыми выборками не наблюдается.
lp4_10 = 0
lp4_10 = lazy_check(493, 248, 0.1)
Сравнение долей по пользователям, открывшим главную страницу: p-значение: 0.29424526837179577 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Сравнение долей по пользователям, открывшим страницу товара: p-значение: 0.43425549655188256 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Сравнение долей по пользователям, перешедшим в корзину: p-значение: 0.18175875284404386 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Сравнение долей по пользователям, совершившим покупку: p-значение: 0.6004294282308704 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
И при значимости в 10% разницы также не наблюдается, значит мы можем передать дизайнерскому отделу, что они смело могут менять шрифты.
Так как мы сравниваем более чем 2 группы, к тому же по нескольким изменениям (в нашем случае доли пользователей, которые побывали на конкретном этапе), стоит учесть поправки на множественное тестирование. Таким образом, мы сможем избежать возникновения ошибок и скорректировать уровень статистической значимости.
Для этого мы проверим наши проведенные тесты на два типа ошибок:
FWER (Family-Wise Error Rate) - Групповая вероятность ошибки, которая представляет собой вероятность получить по крайней мере одну ошибку первого родаFDR (False Discovery Rate) — это среднее значение отношения ошибок первого рода к общему количеству отклонений основной гипотезы#Создадим списки со всеми значениями p_value для каждого уровня статистической значимости
pv_05 = [*lp1_05, *lp2_05, *lp3_05, *lp4_05]
pv_10 = [*lp1_10, *lp2_10, *lp3_10, *lp4_10]
#Вероятность получить хотя бы одну ошибку первого рода для двух значений alpha
print("FWER: " + str(multipletests(sorted(pv_05), alpha=0.05,
method='holm', is_sorted = True)))
print("FWER: " + str(multipletests(sorted(pv_10), alpha=0.1,
method='holm', is_sorted = True)))
print('')
#Среднее значение отношения ошибок первого рода к общему количеству отклонений основной гипотезы
print("FDR: " + str(multipletests(pv_05, alpha=0.05,
method='fdr_bh', is_sorted = False)))
print("FDR: " + str(multipletests(pv_10, alpha=0.1,
method='fdr_bh', is_sorted = False)))
FWER: (array([False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False]), array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]), 0.0032006977101884937, 0.003125)
FWER: (array([False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False]), array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]), 0.006563398416385313, 0.00625)
FDR: (array([False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False]), array([0.80753037, 0.52439501, 0.52439501, 0.52439501, 0.52439501,
0.52439501, 0.52439501, 0.52439501, 0.6672078 , 0.91978178,
0.73899007, 0.80753037, 0.52439501, 0.6672078 , 0.52439501,
0.73899007]), 0.0032006977101884937, 0.003125)
FDR: (array([False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False]), array([0.80753037, 0.52439501, 0.52439501, 0.52439501, 0.52439501,
0.52439501, 0.52439501, 0.52439501, 0.6672078 , 0.91978178,
0.73899007, 0.80753037, 0.52439501, 0.6672078 , 0.52439501,
0.73899007]), 0.006563398416385313, 0.00625)
Использованный метод выводит результат проведения теста в виде буллевых значений, где:
True - нулевая гипотеза отвергается,False - нулевая гипотеза не отвергается; Затем выводится список скорректированных значений p_value для каждого проведенного теста, а также скорректированные значения для уровня статистической значимости по двум методам поправок - Сидака и Бонферрони.
Исходя из полученных нами результатов, ни в одном из тестов нулевая гипотеза не отвергается и стоит использовать уровень статистической значимости в 0.3 - 0.6%, чтобы снизить уровень ложноположительных результатов.
Мы исследовали данные трех групп, пользовавшихся приложением по продаже продуктов питания. Мы выделили следующие особенности данных:
Проведено статистическое тестирование, для определения, будут ли изменения влиять на пользоватей. Для этого было проведено:
По результатам тестирования установлено, что изменение шрифта в приложении не изменит отношения пользователей к приложению и не окажет влияния на ключевое событие. Таким образом, отдел дизайна может ввести свои изменения.